﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace Upreader.Usenet.Nntp.Encodings
{
    /// <summary>
    /// Base class for any encoding stream which has meta lines for containing information
    /// </summary>
    public abstract class MetaLineStream : Stream
    {
        #region Configuration

        #endregion // Configuration

        #region Fields

        private readonly Stream _baseStream;
        private readonly Encoding _encoding;
        private readonly byte[] _metaLinePrefixBytes;

        private byte _currentMetaLinePrefixIndex;
        private StringBuilder _metaLineBuilder;

        #endregion // Fields

        #region Constructor

        public MetaLineStream(Stream baseStream, 
            string metaLinePrefix, 
            Encoding encoding)
        {
            if (baseStream == null)
                throw new ArgumentNullException("baseStream");
            if (metaLinePrefix == null)
                throw new ArgumentNullException("metaLinePrefix");
            if (encoding == null)
                throw new ArgumentNullException("encoding");

            _baseStream = baseStream;
            _encoding = encoding;

             // create a set of prefix bytes usefull for detecting actual line prefixes without 
            _metaLinePrefixBytes = encoding.GetBytes("\r\n" + metaLinePrefix);

            // we start with at the beginning of a new line. Simulate this with position 2
            _currentMetaLinePrefixIndex = 2;
        }

        #endregion // Constructor

        #region Properties

        public Stream BaseStream
        {
            get { return _baseStream; }
        }

        /// <summary>
        /// Always true
        /// </summary>
        public override bool CanRead
        {
            get { return true; }
        }

        /// <summary>
        /// Always false
        /// </summary>
        public override bool CanSeek
        {
            get { return false; }
        }

        /// <summary>
        /// This rawStream is intended for reading only, always false
        /// </summary>
        public override bool CanWrite
        {
            get { return false; }
        }

        public override void Flush()
        {
            _baseStream.Flush();
        }

        /// <summary>
        /// Not supported
        /// </summary>
        public override long Length
        {
            get { throw new NotSupportedException(); }
        }

        /// <summary>
        /// Not supported
        /// </summary>
        public override long Position
        {
            get
            {
                throw new NotSupportedException();
            }
            set
            {
                throw new NotSupportedException();
            }
        }

        #endregion // Properties

        #region Methods

        public override int Read(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (offset < 0)
                throw new ArgumentOutOfRangeException("offset");
            if (count < 0)
                throw new ArgumentOutOfRangeException("count");
            if (count > buffer.Length - offset)
                throw new ArgumentOutOfRangeException("count", "to many bytes were requested for the current buffer when starting from offset");

            // read the next part in the buffer
            int total = _baseStream.Read(buffer, offset, count);

            for (int index = offset; index < total; index++)
            {
                if (_metaLineBuilder == null)
                {
                    byte currentMetaLinePrefixCharacter = _metaLinePrefixBytes[_currentMetaLinePrefixIndex];

                    byte currentCharacter = buffer[index];
                
                    if (currentMetaLinePrefixCharacter == currentCharacter)
                    {
                        // if the _currentMetaLinePrefixIndex is the last readBufferIndex of metaLinePrefixBytes (therefore +1 because were checking against length)
                        if (_currentMetaLinePrefixIndex + 1 == _metaLinePrefixBytes.Length)
                        {
                            // and reset the readBufferIndex since we want to catch a next metaLinePrefixIndex
                            _currentMetaLinePrefixIndex = 0;

                            // start a new metaLineBuilder initially filled with the prefix
                            _metaLineBuilder = new StringBuilder();
                        }
                        else
                        {
                            // This is not the final metaLinePrefixIndex so increase the readBufferIndex for a next catch
                            _currentMetaLinePrefixIndex++;
                        }

                        currentMetaLinePrefixCharacter = _metaLinePrefixBytes[_currentMetaLinePrefixIndex];
                    }
                    else if (_currentMetaLinePrefixIndex > 0)
                    {
                        // special case where a pattern like \r\r\n might occur. 
                        // at this point we are at the second \r but expected a \n
                        // logically, we should only reset the currentMetaLinePrefixIndex to 0
                        // when the character on the currentMetaLinePrefixIndex is NOT the first character on metaLinePrefixBytes
                        // else we should set currentMEtaLinePrefixIndex on 1
                        if (currentCharacter != _metaLinePrefixBytes[0])
                        {
                            _currentMetaLinePrefixIndex = 0;
                        }
                        else
                        {
                            _currentMetaLinePrefixIndex = 1;
                        }
                    }
                }
                else
                {
                    // we are reading a metaLine so keep on Reading untill we found the end of the line
                    char currentCharacter = _encoding.GetChars(buffer, index, 1)[0];
                    _metaLineBuilder.Append(currentCharacter);
                    // if this is the end of the line, notify and return
                    if (currentCharacter == '\n' && _metaLineBuilder[_metaLineBuilder.Length - 2] == '\r')
                    {
                        // get the string copy trimming the endline
                        string metaLineString = _metaLineBuilder.ToString(0, _metaLineBuilder.Length - 2);
                        OnMetaLineFound(metaLineString);

                        // finally, we need to skip the current bytes in the output. Therefore, move each byte after the current one to the front
                        int metaLineLength = _metaLineBuilder.Length + (_metaLinePrefixBytes.Length - 2); // -2 to compensate for the inherent \r\n 
                        int bytesStartIndex = index + 1; // +1 to compensate for the lost index which is still on the metaline
                        int metaLineStartIndex = bytesStartIndex - metaLineLength;
                        int bytesLeft = total - bytesStartIndex;
                        Array.Copy(buffer, bytesStartIndex, buffer, metaLineStartIndex, bytesLeft);
                        
                        index = metaLineStartIndex - 1; // -1 since in the next iteration we will do the actual advancement
                        total -= metaLineLength;
                        _metaLineBuilder = null;
                        //We ended with a lineEnd so the next byte might be the start of a new metaLine
                        _currentMetaLinePrefixIndex = 2;
                    }
                }
            }
            
            return total;
        }

        /// <summary>
        /// Not supported
        /// </summary>
        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Not supported
        /// </summary>
        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Not supported
        /// </summary>
        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new NotSupportedException();
        }

        #endregion // Methods

        #region Disposable

        protected override void Dispose(bool disposing)
        {
            try
            {
                if (disposing)
                {
                    BaseStream.Dispose();
                }
            }
            finally
            {
                base.Dispose(disposing);
            }

        }

        /// <summary>
        /// Parses the metaline specified
        /// </summary>
        protected abstract void OnMetaLineFound(string line);

        #endregion // Disposable
    }
}
